package com.cm.GeoToolsAStart.util;

import org.geotools.data.DataUtilities;
import org.geotools.feature.SchemaException;
import org.geotools.feature.simple.SimpleFeatureBuilder;
import org.geotools.feature.simple.SimpleFeatureImpl;
import org.geotools.geometry.jts.JTSFactoryFinder;
import org.geotools.graph.build.feature.FeatureGraphGenerator;
import org.geotools.graph.build.line.LineStringGraphGenerator;
import org.geotools.graph.path.AStarShortestPathFinder;
import org.geotools.graph.path.Path;
import org.geotools.graph.structure.Edge;
import org.geotools.graph.structure.Graph;
import org.geotools.graph.structure.Node;
import org.geotools.graph.structure.basic.BasicEdge;
import org.geotools.graph.structure.basic.BasicNode;
import org.geotools.graph.traverse.standard.AStarIterator;
import org.locationtech.jts.algorithm.distance.DistanceToPoint;
import org.locationtech.jts.algorithm.distance.PointPairDistance;
import org.locationtech.jts.geom.*;
import org.locationtech.jts.operation.distance.DistanceOp;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;

import java.awt.geom.Point2D;
import java.util.*;
import java.util.stream.Collectors;

public class GeometryUtil {
    private static final LineStringGraphGenerator lineStringGraphGenerator = new LineStringGraphGenerator();
    public static final FeatureGraphGenerator featureGen = new FeatureGraphGenerator(lineStringGraphGenerator);
    private static SimpleFeatureType featureType = null;
    private static final GeometryFactory GEOMETRY_FACTORY = JTSFactoryFinder.getGeometryFactory();
    private static final double D = 1;


    private static final String type = "LineString";

    static {
        try {
            featureType = DataUtilities.createType("Link", "geometry:" + type);
        } catch (SchemaException e) {
            e.printStackTrace();
        }
    }

    /**
     * 根据一个点创建空间点
     *
     * @param x x坐标
     * @param y y坐标
     * @return
     */
    public static Point createPoint(Double x, Double y) throws Exception {
        if (x == null || y == null) {
            throw new Exception("点位异常");
        }
        return GEOMETRY_FACTORY.createPoint(new Coordinate(x, y));
    }

    public static LineString createLineString(Double x1, Double y1, Double x2, Double y2) throws Exception {
        if (x1 == null || y1 == null || x2 == null || y2 == null) {
            throw new Exception("点位异常");
        }
        Coordinate p1 = new Coordinate(x1, y1);
        Coordinate p2 = new Coordinate(x2, y2);
        Coordinate[] coordinates = new Coordinate[]{p1, p2};
        return GEOMETRY_FACTORY.createLineString(coordinates);
    }

    public static Graph createGraph(List<LineString> lineStringList) {
        lineStringList.forEach(line -> {
            SimpleFeatureBuilder featureBuilder = new SimpleFeatureBuilder(featureType);
            featureBuilder.add(line);
            SimpleFeature simpleFeature = featureBuilder.buildFeature(null);
            featureGen.add(simpleFeature);
        });
        return featureGen.getGraph();
    }

    public static LineString findNearestLine(Point2D.Double point, String name, List<LineString> lineStringList) throws Exception {
        Point sourcePoint = createPoint(point.getX(), point.getY());
        return lineStringList.parallelStream().filter(lineString -> ((HashMap) lineString.getUserData()).get("name").equals(name)).min((o1, o2) -> {
            Double o1Dis = o1.distance(sourcePoint);
            Double o2Dis = o2.distance(sourcePoint);
            return o1Dis.compareTo(o2Dis);
        }).orElse(null);
    }

    public static Node getNearestNode(Graph graph, String name, Double x, Double y) throws Exception {
        double minDist = Double.MAX_VALUE;
        Node nearestNode = null;
        Point target = createPoint(x, y);
        Set<Node> nodeSet = new HashSet<>();
        graph.getEdges().stream().filter(edge -> {
            LineString defaultGeometry = (LineString) ((SimpleFeature) edge.getObject()).getDefaultGeometry();
            Map<String, String> userData = (Map<String, String>) defaultGeometry.getUserData();
            return userData.get("name").equals(name);
        }).collect(Collectors.toSet()).forEach(edge -> {
            nodeSet.add(edge.getNodeA());
            nodeSet.add(edge.getNodeB());
        });
        for (Node node : nodeSet) {
            Point object = (Point) node.getObject();
            double distance = DistanceOp.distance(object, target);
            if (distance < minDist) {
                minDist = distance;
                nearestNode = node;
            }
        }
        return nearestNode;
    }

    public static Path AStarShortestPath(Node start, Node end, Graph graph) throws Exception {
        Point endPoint = (Point) end.getObject();
        if (graph == null) {
            System.out.println("graph不存在，请构建graph");
            return null;
        }
        if (start.equals(end)) {
            System.out.println("起点和终点相同，请重新选点");
            return null;
        }
        AStarIterator.AStarFunctions aStarFunctions = new AStarIterator.AStarFunctions(end) {
            @Override
            public double cost(AStarIterator.AStarNode start, AStarIterator.AStarNode end) {
                Edge edge = start.getNode().getEdge(end.getNode());
                SimpleFeature feature = (SimpleFeature) edge.getObject();
                Geometry geometry = (Geometry) feature.getDefaultGeometry();
                return geometry.getLength();
            }

            @Override
            public double h(Node node) {
                Point nodePoint = (Point) node.getObject();
                double dx = Math.abs(nodePoint.getX() - endPoint.getX());
                double dy = Math.abs(nodePoint.getY() - endPoint.getY());
                return D * (dx + dy);
            }
        };
        AStarShortestPathFinder pathFinder = new AStarShortestPathFinder(graph, start, end, aStarFunctions);
        pathFinder.calculate();
        Path path = pathFinder.getPath();

        return path;
    }

    public static List<Point2D.Double> getShortPath(Double x1, Double y1, String region1, Double x2, Double y2, String region2, Graph graph) throws Exception {
        List<Point2D.Double> result = new ArrayList<>();
        Node starNode = GeometryUtil.getNearestNode(graph, region1, x1, y1);
        Node endNode = GeometryUtil.getNearestNode(graph, region2, x2, y2);
        Path path = AStarShortestPath(starNode, endNode, graph);

        LineString startPath = (LineString) ((SimpleFeatureImpl) ((BasicEdge) path.getEdges().get(0)).getObject()).getDefaultGeometry();
        LineString endPath = (LineString) ((SimpleFeatureImpl) ((BasicEdge) path.getEdges().get(path.size() - 2)).getObject()).getDefaultGeometry();

        path.forEach(node -> {
            Point nodeObject = (Point) ((BasicNode) node).getObject();
            Point2D.Double xyPoint = new Point2D.Double(nodeObject.getX(), nodeObject.getY());
            result.add(xyPoint);
        });

        Collections.reverse(result);

        result.add(0, new Point2D.Double(x1, y1));
        if (!Objects.isNull(findCloseLinePoint(createPoint(x1, y1), startPath))) {
            // TODO: 2023/2/27 去除第一个node
            result.remove(1);
        }

        result.add(new Point2D.Double(x2, y2));
        if (!Objects.isNull(findCloseLinePoint(createPoint(x2, y2), endPath))) {
            // TODO: 2023/2/27 加上尾结点
            result.remove(result.size() - 2);
        }
        return result;
    }

    /**
     * 查找点在已知线段上的投影点
     *
     * @param sourcePoint 查找点
     * @param lineString  目标线
     * @return
     */
    public static Point2D findCloseLinePoint(Point sourcePoint, LineString lineString) {
        Coordinate point = new Coordinate(sourcePoint.getX(), sourcePoint.getY());
        PointPairDistance pointPairDistance = new PointPairDistance();
        DistanceToPoint.computeDistance(lineString, point, pointPairDistance);
        Coordinate result = pointPairDistance.getCoordinate(0);

        if (sourcePoint.getCoordinate().distance(lineString.getCoordinateN(0))>lineString.getLength() || sourcePoint.getCoordinate().distance(lineString.getCoordinateN(1))>lineString.getLength()){
            return null;
        }
        return new Point2D.Double(result.getX(), result.getY());
    }

}
